release: v1.4.10 — LTC portfolio stability, UTXO audit recovery, XRP REST signing, Hive onboarding#327
Merged
Merged
Conversation
v1.4.6 published as prerelease.
applyPolicy raised the device's ENABLE/DISABLE POLICY confirm but bypassed emuSigningOp, so on the emulator no interactive button was rendered and the op hung in confirm_helper until the 120s timeout. Route it through emuSigningOp when engine.isEmulator, matching every other device-confirm op. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…TL, 600s timeout Vault-side fixes for the BEX 're-approve every time' bug (handoff from keepkey-client PR #85). Four defects in the pairing store: 1. Idempotent pairing: approvePairing()/pair() now reuse a live key for the same identity instead of minting a new UUID + row every time. Identity is a client-supplied clientId (new, optional) or the (name,url,imageUrl) triple. reloadPairings() collapses pre-existing duplicate rows on load. POST /auth/pair returns { apiKey, reused }. 2. LRU eviction: evictIfFull() drops the least-recently-used key, not the oldest-inserted — so the actively-used key is never evicted at capacity. 3. Sliding TTL: validate() measures the 30d TTL against last use and refreshes recency (persist throttled to ~1x/hour). Active keys never expire; abandoned ones still age out. 4. Timeout: pending-pair auto-reject raised 60s -> 600s to match the SDK's POST /auth/pair wait, so a slow approval isn't discarded. Also: coalesce concurrent same-identity pair requests onto one prompt (no 429); DELETE /auth/pair revokes the caller's own key; nullable client_id + last_used_on columns on paired_apps (backward-compatible migration). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix: emulator policy confirm button + pairing re-approve churn
…nloading…" (#284) * fix(update): route updates to keepkey.com/update page, drop in-app "Downloading…" Desktop update on macOS/Windows opened the GitHub releases page and the UI optimistically showed a "Downloading…" progress bar even though nothing downloads in-app. Now: - Open https://keepkey.com/update?os=<mac|windows|linux>&arch=<arm64|x64> &version=<latest>¤t=<installed> so the page can serve the correct build for the user's OS/arch and guide them through updating. - Remove the optimistic 'downloading' phase in useUpdateState. Linux's native updater still drives real downloading/ready phases via status events; the open-the-page flow no longer shows a fake progress bar. - Relabel the error-fallback button "Download from GitHub" -> "How to Update". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(update): quote URL for Windows cmd start; translate 'How to Update' in all locales - Windows: '&' in the query string is a cmd separator; quote the URL so 'start' opens the full keepkey.com/update link instead of splitting at the first '&'. - Update downloadManually copy in all 14 non-English locales (was stale 'Download from GitHub' while the button now opens the update page). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#285) * fix(watch-only): make Refresh re-fetch balances from cached addresses Watch-only mode (viewing a cached device snapshot with no device connected) showed a Refresh button that did nothing: Dashboard.refreshBalances early- returned on `watchOnly`, and getWatchOnlyBalances only read the DB cache. Add a refreshWatchOnlyBalances RPC that reconstructs the pubkey list from cache (cached chain addresses + cached BTC xpubs), re-queries Pioneer's GetPortfolioBalances (no device), parses natives + tokens + DeFi into ChainBalance[], persists, and returns. The device getBalances path and its managers are untouched — this is a self-contained, display-only parse (no per-owner EVM token map, no addressbook sync). Frontend refreshBalances now runs this path in watch-only mode with proper loading state and error surfacing instead of returning silently. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(watch-only): don't overwrite cached balances with confirmed zeros on partial Pioneer failure Track which chunks failed and exclude their chains from the confirmed set so setCachedBalances uses the guarded upsert (keeps existing non-zero) instead of forcing a 0 — matching the device getBalances behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nnected' (#286) * fix(watch-only): show cached receive address instead of 'No device connected' In watch-only mode there is no connected device, so every *GetAddress RPC threw 'No device connected' and the Receive page rendered that error for show-address, QR, and copy — even though the address is cached from the last connection. - AssetPage.deriveAddress and the BTC account-aware effect now short-circuit in watch-only and display the cached address (balance.address) instead of calling the device RPC. - ReceiveView gains a watchOnly prop: hides Verify-on-Device, the BTC receive/change + index stepper, the TON bounceable toggle, and the edit-path pencil — all device-derive actions that can't work offline and would show a stale cached address. Path text stays visible (informational). - Empty offline state shows a plain explanation instead of device-hitting retry/derive buttons. Connected-device flow is unchanged (all new branches gate on watchOnly). * fix(watch-only): stop EVM effect clearing cached address; hide account selectors - EVM address effect now returns early in watch-only instead of setAddress(null) when the device-backed evmAddresses hook is empty (which it is offline), preserving balance.address as the watch-only source of truth. - BtcXpubSelector / EvmAddressSelector are gated behind !watchOnly — their select/add/remove handlers call device/backend account RPCs and could mix live account state with the cached receive address. Addresses PR #286 review findings 1 (high) and 2 (medium).
…rong seed (#287) The emulator flash is not a trustworthy seed store — the firmware's storage wrapping key changes per kkemu_init (it can't decrypt its own sec section across restarts), so a persisted flash can boot a DIFFERENT seed than the one the user saved as a mnemonic. The connect smoke-test passes on any valid seed, so the wrong wallet was presented as ready with no warning. - Deterministic seed-identity guard: compare the live m/44'/0'/0' BTC xpub (already fetched by the smoke-test) against the xpub derived offline from the saved mnemonic; on mismatch, wipe the flash and reload the saved seed. - Blank-flash recovery: if the flash boots uninitialized but a mnemonic is saved for the wallet, restore the saved seed instead of dropping into setup. - reloadSavedSeed verifies the live xpub after reload and errors if it still does not match — no path reports success without proving the seed. - Consolidated the storage-stale recovery through reloadSavedSeed, replacing the duplicated inline reload + flaky DebugLink mnemonic verify.
v1.4.7 published as prerelease.
…t ship (#290) The proto-tx-builder / hdwallet / device-protocol dist/ output is gitignored and copied into the app bundle via file: refs. Their build stamps track src file mtimes, but a submodule `git checkout` to a new pin does not reliably bump src mtimes past an existing stamp — so make considers the build current and skips it, shipping a STALE dist from a previous commit. This shipped a pre-fix proto-tx-builder in v1.4.6 and v1.4.7: the source had the @cosmjs/stargate Freegrant/Feegrant typo fallback (commit f12f8c3, which both releases pinned), but the bundled dist predated it, so every Cosmos transaction crashed with 'createFeegrantAminoConverters is not a function' (send, delegate, undelegate). Clear the module build stamps in build-signed (same pattern already used for the zcash-cli stamp) so the release always rebuilds modules from the pinned source before the vault install copies their dist into the bundle.
…d string (#289) The swap dialog header rendered the literal 'Swaps.pro' (an unrelated third-party brand) instead of the localized title. de610e8 replaced the t('title') i18n key with a hardcoded string inside a UI styling refactor; this also broke localization of the title in all languages. Revert to t('title') (= 'Swap').
v1.4.8 published as prerelease.
….16→7.15 Add a user-facing `hiveEnabled` toggle to the Settings drawer, default OFF, mirroring the zcashPrivacyEnabled pattern. Hive is hidden from the Dashboard grid, command palette, and all backend chain lists unless the flag is on. Also corrects the Hive firmware gate from 7.16.0 to 7.15.0. The Hive handler ships in firmware 7.15.0 (alpha) — same release as Zcash — but the gate was set to 7.16.0 (the device-protocol package version, not the firmware version), which no firmware reports, so Hive could never appear even on supported devices. The new default-off settings flag now carries "is Hive ready for users" instead. - shared/types.ts, rpc-schema.ts: hiveEnabled on AppSettings + setHiveEnabled RPC - bun/index.ts: persist + getAppSettings + handler (fw-gated 7.15.0) + 4 chain-filter sites - chains.ts: minFirmware 7.16.0 → 7.15.0 - DeviceSettingsDrawer/Dashboard/CommandPalette/App: toggle UI + flag plumbing
…scan Addresses review findings — the settings flag gated UI display sites but several backend data paths still served Hive on firmware alone: - REST /api/health no longer advertises Hive, and /addresses/hive returns 403 when hive_enabled=0 (was firmware-only) — matches the /api/zcash gate. - getCachedBalances drops stale Hive rows when the flag is off, so disabling Hive after enabling it no longer leaks cached balances into portfolio totals. - Dashboard total (cleanBalanceUsd) skips Hive when off so stale in-memory rows from a live-refresh merge can't be counted while hidden from the grid; added an auto-refresh-on-enable effect mirroring Zcash. - Background + single-chain activity scans filter Hive when off (was deriving on any firmware-eligible device). - CommandPalette results memo now depends on firmwareVersion (was stale). - types.ts comment corrected 7.16.0 → 7.15.0.
feat(hive): settings feature flag (default off) + fix firmware gate 7.16→7.15
…g plan Vault derived Hive keys at m/44'/1275'/0'/0/0 (BIP-44) while firmware 7.15.0, Ledger, and Hive Keychain all use SLIP-0048 m/48'/13'/role'/account'/key'. The old path produced a STM key no other Hive tool derives from the same seed — non-interoperable, and Pioneer's pubkey→account lookup could never match an account made elsewhere. Safe breaking change: Hive is flag-gated default-off (PR #293), so zero accounts exist on v1 keys. - chains.ts: hive defaultPath → m/48'/13'/1'/0'/0' (active role) - chains.ts: add hiveRolePath(role, accountIndex) SLIP-0048 helper for the upcoming multi-key / account-creation work - docs/HIVE-ONBOARDING-PLAN.md: phased next-steps for self-run sponsor onboarding (decisions: KeepKey-owners-only gating, queue+circuit-breaker, single account, active-key only) GATE before flag-on: verify the derived STM active key matches Hive Keychain output for the same device seed.
… baseline sig gate
Byte-exact spec derived from firmware 7.15.0 hive.c: the digest the device signs (SHA256(chain_id||serialized_tx)), 65-byte sig recovery, account_create op-9 serialization layout, and Pioneer's verification algorithm. Clears the 'blocked on firmware attestation-digest' status — no firmware change needed; device returns serialized_tx, Pioneer verifies + parses the returned bytes.
…tAminoConverters is not a function') (#295) The Windows production build's 'Building proto-tx-builder' step only ran `bun install` — it never built the dist (tsc). proto-tx-builder/dist is gitignored, so Windows shipped whatever stale pre-pin dist was on the box. v1.4.6/1.4.7/1.4.8 thus shipped a proto-tx-builder compiled BEFORE the @cosmjs/stargate Freegrant/Feegrant typo shim (f12f8c3), so every Cosmos send/delegate crashed with 'createFeegrantAminoConverters is not a function'. macOS was fixed in #290 (clear module stamps in build-signed); the Windows pipeline is separate and was never patched. Add `bun run build` after the install (mirrors the hdwallet yarn build right below) so Windows rebuilds the dist from the pinned source. Release hotfix — not RUJI-related; fast-track / cherry-pick to a release branch.
* test(thorchain): RUJI/TCY/secured-asset custom-denom signing + hdwallet bump
Adds a keepkey-sdk regression test exercising THORChain MsgSend with
arbitrary denoms (TCY, RUJI/Rujira, secured assets like btc-btc) and
MsgDeposit with memo against a running vault. Bumps the hdwallet
submodule to the matching allow-any-denom MsgSend fix.
* chore(hdwallet): pin to merged develop (allow-any-denom MsgSend)
* feat(thorchain): send + display THORChain bank tokens (TCY, RUJI, secured assets)
Threads a per-asset bank denom through the cosmos tx builder so a
THORChain MsgSend can move TCY (denom 'tcy'), RUJI ('x/ruji') or secured
assets — the fee stays in RUNE. Denom comes from a '/bank:<denom>' caip
segment, parsed greedily so denoms containing '/' survive. Token MAX
sends the full token balance (native fee is a separate RUNE balance, not
reserved against the token).
Display: classify '/bank:' caips as tokens so they group under THORChain
like ERC-20s. Unit test covers RUNE-native regression + TCY/RUJI/MAX.
Caip contract with Pioneer: cosmos:thorchain-mainnet-v1/bank:<denom>.
Surfacing balances still requires Pioneer to enumerate bank balances in
the portfolio (currently native-only).
* fix(emulator): stop WKWebView SIGTRAP crash on confirm into a not-ready webview
sendToWindow injected `window.handlePacket(...)` via executeJavascript; if
the webview's page hadn't loaded handlePacket yet, the JS error crashed the
WKWebView process with EXC_BREAKPOINT (SIGTRAP, exit 133) and took down the
app. viewReady gated this but was only reset on window close — a stale
`true` (e.g. webview rebuilt without a close event, as in an ethSignMessage
confirm right after reopen) let the inject fire into a fresh, unloaded
webview.
- Guard the injected JS: `if(window.handlePacket)...` — a not-ready inject
is now a no-op, never a WebKit crash.
- Reset viewReady=false at openEmulatorWindow() so a reopen can't inherit a
stale-ready gate.
* fix(thorchain): match Pioneer '/denom:' caip scheme + un-flag TCY/RUJI as spam
Pioneer emits bank tokens as cosmos:thorchain-mainnet-v1/denom:<denom>
(e.g. denom:tcy, denom:x/ruji), not /bank:. Align the send denom
extraction and the display token-denom regex to /denom: (bank: kept for
forward-compat). Allowlist THORChain/Maya /denom: tokens in the spam
filter — they're protocol-defined (no counterfeits possible) and not yet
in the discovery catalog, so TCY/RUJI were wrongly flagged SCAM.
* feat(swap): make THORChain TCY and RUJI tradable in the swapper
THOR.TCY and THOR.RUJI have live THORChain pools but weren't selectable.
- getSwapAssets: defensive shim adds THOR.TCY/THOR.RUJI (keyed by their
/denom: caip) if Pioneer's available-assets omits them, mirroring the
existing RUNE/TRON shims — so they appear in the picker under THORChain.
- assessAvailability: allowlist the two pooled /denom: caips as swappable
via thorchain. Scoped to TCY/RUJI only — other non-pooled thorchain
denom tokens correctly stay unsupported. Quotes route by caip through
Pioneer.Quote (THOR.TCY/THOR.RUJI pools verified on thornode).
* fix(thorchain): send/swap TCY & RUJI correctly (denom: token + MsgDeposit)
Two confirmed bugs sending/swapping THORChain bank tokens:
1. SendForm.isTokenSend didn't recognize the /denom: namespace, so picking
TCY left token-mode off: the amount showed RUNE and the caip never
reached the builder — it built a RUNE send. Add /denom: to the check.
2. Swapping FROM TCY/RUJI failed 'Missing inbound vault address from quote'.
These are on-chain bank tokens: Pioneer's quote returns txs[0].type=
'deposit' with no inbound address (MsgDeposit). isNativeDepositCaip only
matched RUNE/CACAO slip44 — extend it to thorchain/maya /denom: caips.
The MsgDeposit coins asset was also hardcoded THOR.RUNE; thread the real
asset (THOR.TCY/THOR.RUJI) via a new depositAsset param so the network
takes the right coin.
cosmos.test.ts: +MsgDeposit case (asset=THOR.TCY, swap memo). 10/10.
* fix(swap): TCY/RUJI quote parser threw 'missing inbound address' too
The quote parser (swap-parsing.ts) had its OWN isNativeDeposit check
matching only RUNE/CACAO slip44 — it threw before the build path's check
(already fixed) ever ran, so from-TCY/RUJI swaps still failed.
Dedupe: single exported isNativeDepositCaip in swap-parsing.ts (covers
RUNE/CACAO + thorchain/maya /denom: bank tokens), imported by swap.ts.
One source of truth so the parser and builder can't disagree again.
Removes now-orphaned RUNE_CAIP/CACAO_CAIP constants.
* fix(swap+emulator): correct deposit preview asset + don't reject confirm before webview ready
- swap preview built the MsgDeposit with the default THOR.RUNE instead of
THOR.TCY/THOR.RUJI (depositAsset wasn't threaded on the preview path, only
execute). Preview now shows the real coin.
- Emulator confirm waited only 5s for a freshly-opened webview to post
/_emu/ready, then auto-rejected ('Transaction rejected by user'). A cold
webview on make dev exceeds that. Bump to 20s (loop still exits the instant
viewReady flips, so it only raises the ceiling) so a swap confirm isn't
spuriously rejected before the window is up.
* fix(windows-build): build proto-tx-builder dist (fixes 'createFeegrantAminoConverters is not a function')
The Windows production build's 'Building proto-tx-builder' step only ran
`bun install` — it never built the dist (tsc). proto-tx-builder/dist is
gitignored, so Windows shipped whatever stale pre-pin dist was on the box.
v1.4.6/1.4.7/1.4.8 thus shipped a proto-tx-builder compiled BEFORE the
@cosmjs/stargate Freegrant/Feegrant typo shim (f12f8c3), so every Cosmos
send/delegate crashed with 'createFeegrantAminoConverters is not a function'.
macOS was fixed in #290 (clear module stamps in build-signed); the Windows
pipeline is separate and was never patched. Add `bun run build` after the
install (mirrors the hdwallet yarn build right below) so Windows rebuilds the
dist from the pinned source.
Release hotfix — not RUJI-related; fast-track / cherry-pick to a release branch.
* feat(thorchain): gate TCY/RUJI behind firmware 7.15 (fund safety)
A bank-token MsgSend's denom field is honored only from firmware 7.15.0;
on older firmware it's ignored and the tx signs as RUNE — the user would
move RUNE thinking they moved TCY. Gate the whole TCY/RUJI feature on 7.15
using the existing minFirmware/assessWithFirmware pattern, extended to the
asset level (RUNE itself stays usable on older fw).
Single shared helper thorchainBankTokenFirmwareOK() in swap-support-matrix,
enforced at three layers:
- assessWithFirmware: dims TCY/RUJI in the swap picker with an upgrade hint
- headlessSwapQuote: refuses a quote when selling a bank token on old fw
- buildTx handler: backend fund-safety net — refuses to build the send/swap
tx, so a bad asset can never reach signing
11/11 gate tests; assessWithFirmware verified (7.14→unsupported, 7.15→ok).
* fix(hdwallet): pin to e39ff8cb (master + RUJI denom fix), not the develop squash
PR review caught that the prior pin 8c8e5b01 was a develop squash-merge that
does NOT have c5a4d79b (hdwallet master) as an ancestor — it dropped master-only
work (WebUSB clearHalt, Zcash, TON/TRON, BIP-85, message-type registry, EVM
clear-signing). e39ff8cb is c5a4d79b + ONLY the denom fix, so the bump is now a
fast-forward that preserves all master history.
* fix(swap): TCY/RUJI token identity + enforce firmware gate at execute time
Two PR-review release blockers:
- Token identity (was falling through to RUNE balance): the getSwapAssets
shim injected THOR.TCY/THOR.RUJI without a contractAddress, so SwapDialog's
contractAddress-keyed token lookup missed them and used the native RUNE
balance for max/validation/price. Set contractAddress to the bank denom
('tcy' / 'x/ruji') — matches the portfolio token's contractAddress (the
denom, set by getBalances), so balance now resolves to the actual token.
- Execute-time firmware gate: the 7.15 bank-token gate was quote-only, but
/api/v2/swap/execute (rest-swap.ts) calls headlessExecuteSwap directly,
bypassing the quote check. A stale/crafted execute payload could reach
signing on old firmware that signs the wrong asset. Re-check the invariant
at execute time too (mirrors headlessSwapQuote + buildTx).
* chore(deps): bump pioneer-discovery 10.0.9 → 10.3.1 (RUJI/TCY caips + icons)
10.3.1 carries the correct THORChain bank-token caips
(cosmos:thorchain-mainnet-v1/denom:{tcy,x/ruji}) WITH icons and zero stale
thorchain-1/slip44:ruji — so RUJI/TCY now resolve names+icons from the
discovery catalog (fixes the broken portfolio icon) and are no longer
flagged spam at the source.
10.3.1 is the clean republish of 10.3.0 (which shipped a broken
@pioneer-platform/pioneer-caip: workspace:* that no external consumer could
install). Drop the direct pioneer-caip dep — it resolves transitively via
discovery's ^9.27.10. bun.lock is gitignored; release does a fresh resolve.
NOTE: re-validate the Windows MAX_PATH 260-assertion on the release build —
discovery's nested ethers/@ethersproject chain is the recent (v1.4.5)
long-path offender; PR #280's dedup should absorb it but confirm.
* chore(hdwallet): bump to 25db4467 (RUJI denom fix + EIP-712 eth-sig-util→eip-712)
Submodule now carries both fixes on fix/thorchain-any-denom (ff-safe
descendant of hdwallet master c5a4d79b): the THORChain bank-token denom
support and the EIP-712 struct-hash swap that drops @metamask/eth-sig-util
+ its nested @ethereumjs@4 tree (Windows MAX_PATH cleanup).
* chore(hdwallet): re-pin to 91a9e9d6 (EIP-712 commit amended: prettier lint fix)
Same two ff-safe commits (RUJI denom + EIP-712 swap); the EIP-712 commit was
amended to satisfy the hdwallet prettier/lint CI on the regression test. No
logic change. Old 25db4467 superseded.
* fix(swap): import pioneer-discovery chains via ./chains export (10.3.1 exports map)
pioneer-discovery 10.2.0+ added a strict package exports map; the raw deep
path @pioneer-platform/pioneer-discovery/lib/chains.json is no longer
resolvable (vite: Missing "./lib/chains.json" specifier), breaking the vault
build after the 10.3.1 bump. Use the exposed ./chains subpath (same
lib/chains.json, with types). Only deep import in the tree; the other three
discovery imports use the main entry.
* chore(device-protocol): pin fork master 98ca1e2 (ThorchainMsgSend.denom)
Bumps modules/device-protocol f2c3c005 -> 98ca1e2 (BitHighlander/device-protocol
origin/master). Adds the denom field to ThorchainMsgSend (proto commit c1dea44),
which hdwallet's send.setDenom() needs for TCY/RUJI/secured-asset MsgSend.
lib/ is a gitignored build artifact; 'make' regenerates it via 'npm run build'
(build:js + postprocess; the broken build:json is not in that path), so the
shipped lib gets setDenom from a clean checkout. Verified on-device: ruji.js 8/8
(rune/tcy/rujira/btc-btc/eth-usdc MsgSend + MsgDeposit + charset-guard reject).
v1.4.9 published as prerelease.
fix(hive): correct derivation path to SLIP-0048 (Phase 0) + onboarding plan
…300) * feat(hive): sponsor-backed account onboarding + ETH anti-drain gate Hive receive/onboarding panel (HiveAccountPanel) on the Hive asset page: resolves the device's active key to an account via Pioneer, or runs the in-app sponsor onboarding wizard (live username availability, device-derived SLIP-0048 role keys, @keepkey-sponsored on-chain creation; keys never leave the device). Backend RPCs: hiveGetRoleKeys, hiveGetAccount, hiveUsernameAvailable, hiveCreateAccount (device owner-signed account_create attestation -> Pioneer sponsor endpoint). hiveSignTx/hiveSignAccountCreate route through emuSigningOp on the emulator for the interactive confirm gate. ETH anti-drain gate: hiveCreateAccount signs a fixed EIP-191 message bound to username+ownerKey with the device ETH key (m/44'/60'/0'/0/0) and sends ethAddress+ethSignature. The server requires the address to hold mainnet ETH (one sponsored account per funded address) to keep the free service from being drained. 403 surfaces a dedicated 'fund your ETH address' screen. Bumps hdwallet 91a9e9d -> d51727e4 (master) for the Hive account-lifecycle wallet methods (keepkey/hdwallet#50). * fix(hive): address PR review — success poll, derive-error surfacing, username race - P1: after a successful create, poll hiveGetAccount (backing off, 90s cap) and only hand off to the parent once the account resolves, instead of a single 4.5s refresh that could bounce a real success back to the wizard when the pubkey->account lookup lagged. Poll halts on unmount. - P2: surface address-derivation failures instead of an infinite spinner — the panel now takes loading/deriveError/onRetryDerive; a null activeKey with an error (or no in-flight derive) shows an actionable Retry. - P2: ignore stale username-availability responses (tag avail with its name + latest-name guard) so a late 'available' can't enable creation for a different, unchecked name. create() guard hardened to match. * fix(hive): guard the View account button against the success bounce too The auto-poll already waited for hiveGetAccount to resolve before handing off, but the manual 'View account' button still called onCreated blind — a click before Pioneer resolved the pubkey would refresh()->noAccount->wizard, unmounting the celebration. Both paths now share a guarded resolveToAccount(): the button does the same lookup, keeps the success screen on noAccount, and shows a 'still finalizing' hint instead of bouncing.
…K (+ #272 wrong-word test) (#306) * feat(recovery): expose cipher-recovery character entry over REST + SDK + #272 test Adds REST endpoints so an SDK/host can drive on-device cipher recovery (the engine already had the machinery — sendCharacter/Delete/Done — wired only to the in-app UI): POST /system/recovery/character {character} POST /system/recovery/character/delete POST /system/recovery/character/done GET /system/recovery/state -> { word_pos, character_pos, seq } Mirrors /system/recovery/pin. engine-controller tracks the last CharacterRequest (transport event 80) + a seq counter so a caller can sync sends with the device. SDK: sdk.system.recovery.sendCharacter/sendCharacterDelete/sendCharacterDone/getRecoveryState. Test tests/recovery/wrong-word.js validates firmware #272 (per-word BIP-39 validation): drives recovery and enters a guaranteed-invalid word (5 identical ciphered chars -> never a wordlist word, dodges auto-complete regardless of the unreadable scramble), asserts recoverDevice() rejects with "Word not found". Note: lib/ is a build artifact — run `npm run build` (or test:device) before the test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(recovery): address PR #306 review — namespace, lib, state reset, timeout - SDK namespace: move sendCharacter/Delete/Done/getRecoveryState into a new sdk.system.recovery.* namespace (matches the test + docs; sendPin stays in device for back-compat). Fixes the test throwing on sdk.system.recovery.*. - Stale lib: commit rebuilt lib/index.{js,d.ts}(+maps) so published main/types match src (this also catches up pre-existing un-built methods e.g. solana offchain / TIP-191 that were already in src but never built). - State leak: reset lastCharacterRequest + characterRequestSeq + recoveryActive in clearWallet() (disconnect/re-pair) so /system/recovery/state can't return stale progress. Add an explicit marker; recover-device wraps the call in try/finally so state is active only during entry. seq stays monotonic. - Timeout: wipe/resetDevice/recoverDevice/loadDevice now use the SDK signing timeout (600s) instead of the 30s default, since they block on human confirm. Follow-up (noted): bind /system/recovery/state to the initiating session/bearer token if it becomes public API (it exposes only word/char position, no secrets). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
These were created in prior sessions but never committed: - tests/evm-firmware/ (6): on-device clear-sign verifiers for firmware #255/#260/#261 (eip1559 chainid/recover, sellToUniswap offset, thorchain router pin, transformERC20 clear-sign, uniswap liquidity recipient). - tests/recovery/load-verify.js: wipe + loadDevice(random seed) + verify the device derives the same ETH address ethers does (seed-derivation check; distinct from the #272 cipher-recovery wrong-word.js already on develop). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… chain (#307) On a fresh wallet, picking a chain from the "Add a blockchain" grid drilled into it but the Receive/Send/Swap action row was gated behind hasAnyBalance. With no funds anywhere, hasAnyBalance is false, so the row never rendered and new users could not press Receive — completely blocked from getting a deposit address. Ungate the drilled-chain action row from hasAnyBalance (the point of picking a chain is to receive on it), and suppress the generic "pick a chain" welcome empty-state while a chain is drilled so it doesn't sit above the action row. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: firmware-release SOP — upstream-first dependency gating Documents the rule that firmware releases pin UPSTREAM device-protocol + python-keepkey masters, and nothing merges to fork develop until those proto/ test deps are merged upstream first (peer-review gated, days). Corrects the Phase-5-last ordering in the 7.14.0 release plan, which had it backwards. * docs: clarify mergability is in-order (sequential pipeline), not per-branch isolation
…n parse (#297) build-windows-production.ps1 contained UTF-8 em-dashes (U+2014). The file has no BOM, so PowerShell 5.1 reads it in the system ANSI codepage; on a non-UTF-8 codepage the em-dash bytes (E2 80 94) mis-decode and, inside the line 424 "build failed for proto-tx-builder ..." string, produce a stray quote that closes the string early -> the whole script fails to parse and the Windows build never starts. (Introduced with the proto-tx-builder dist build in #295.) Replace every em-dash with ASCII "--". Comments and the one in-string use are now codepage-independent. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…motion gate (#282) v1.4.6 shipped as a prerelease validated only by local/static checks. Local Orchard proof-verify can't distinguish a good tx from a doomed one (the NU6.2 incident proved only a node broadcast can), and the new fail-closed validation could also false-abort a legitimate send. This checklist captures the real-device send/shield/deshield + anti-bleed smoke that must pass before promoting v1.4.6 from prerelease to latest/stable.
#275) Pins that the real-spend tree/witness layer is consensus-sound: a note in a COMPLETED shard below other (insert()ed-root) shards + a frontier shard produces a witness that recomputes the tree root. Uses the existing assert_witness_recomputes_root helper (whose failure is exactly the chain's 'could not validate orchard proof'). This rules the witness/anchor OUT as the cause of that error on real z→z spends — narrowing it to the Orchard proof's other public inputs. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nd-signing (#303) The signing-approval overlay flags any tx the Vault's calldata decoder can't read (source:'none') as needsBlindSigning, which forces AdvancedMode ON. But AdvancedMode is the firmware's single blind-sign switch, so enabling it disables the device's own blind-sign gate globally — re-opening the drain vector the firmware router-pin fix (#261) closed, for any contract the firmware clear-signs natively but the Vault decoder doesn't recognize (e.g. a THORChain plain deposit forced the user to enable AdvancedMode). Add the missing firmware-handled selectors to the local calldata decoder so they decode (source:'local') and don't trip the gate: - THORChain deposit() 0x1fece7b4 (only depositWithExpiry was covered) - 0x sellToUniswap 0xd9627aa4 - Uniswap addLiquidityETH 0xf305d719 The device stays authoritative — it re-checks router/offset/recipient and rejects spoofed variants regardless of what the overlay displays. Also add tests/evm-firmware/ SDK tests that drive the live Vault REST API to a real device and verify the firmware EVM fixes via signer-recovery (#255/#260/ #261): EIP-1559/RLP pre-image, transformERC20 clear-sign, THORChain router pin, sellToUniswap offset validation, addLiquidity recipient guard. Verified on-device against firmware 7.15.0 (da02a7b7). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…298) Two defects made the wallet look Bitcoin-only when a user added another chain (reported by multiple users on v1.4.2 — 'the only thing I can click is Get BTC address' after Add a blockchain → Dash): 1. Dashboard: the 'Add a blockchain' picker only drilled the dashboard viz (setDrilledChainId) and never opened the chosen chain's page, so the hardcoded 'Get BTC address' welcome CTA stayed the only obvious action. It now opens the picked chain's Receive page. 2. index.ts: btcGetAddress is shared by every UTXO chain, but cached the derived address under the literal 'bitcoin' key — so a Dash/LTC/DOGE address overwrote Bitcoin's cache and never persisted under its own chain. The cache key now resolves from params.coin. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WalletConnect pairing used a macOS-only one-shot `screencapture`
("Scan screen") that required Screen Recording TCC permission and
decoded a single static PNG — flaky when the send/address page's
live-camera scanner (QrScannerOverlay, jsqr at ~10fps with retry)
already worked reliably.
Point WalletConnect at the same QrScannerOverlay component: a "Scan
QR" button opens the live camera, validates the decoded string is a
`wc:` URI, and pairs. Drop the dead `wcScanScreen` backend RPC, its
schema entry, and the now-unused PNG-decode helper.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Sync button (zcashShieldedScan) only did an incremental scan — instant and a no-op when already at chain tip, and it never re-checked the cached FVK against the device. #274 wired the device-match into the balance and send paths but NOT the manual Sync, so pressing Sync did no real work. Now Sync resets the verified flag and runs ensureZcashDeviceMatch every time: a forced device round-trip that purges the stale FVK + notes and re-derives on mismatch. This makes Sync do real work AND lets the user clear a stale/bled wallet from the UI without manually deleting ~/.keepkey/zcash_wallet.db. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(audit): track & recover UTXO-altcoin funds beyond account 0 Users report (e.g. LTC) that after updating, the audit finds funds but shows 'Funds here, but KeepKey can't track this path in-app' with only a support handoff. Cause: getBalances/getBalance derive only account 0 for non-BTC UTXO chains, and the audit's canTrack gate excluded UTXO families entirely — so discovered account-1+ funds were stranded. Tracks them the device-ID-scoped way (NOT EVM's global-setting model): - New addUtxoAccount RPC persists a discovered account's xpubs to the existing device-scoped cached_pubkeys table (PK device_id,chain_id,path), gated !isPassphraseWallet exactly like the BTC xpub cache. A hidden wallet's xpubs never touch disk and accounts never bleed across devices. - getBalances (portfolio) and getBalance (send) now merge device-cached altcoin xpubs beyond account 0 (dedup by xpub), so recovered funds are both visible AND spendable. - AuditDialog: trackLevel gains a utxo branch; canTrack includes utxo only for the standard wallet (hidden stays support-handoff, since the cache can't hold it). Copy is now honest: 'shows from now on'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(audit): actually spend tracked account-1+ UTXO-altcoin funds The audit "track" flow persisted account-1+ xpubs and merged them into the balance-display RPCs, but the spend path (buildTx non-BTC UTXO branch) still derived account-0 xpubs only. Result: funds beyond account 0 were shown and offered by max-send but could not be spent (buildUtxoTx coin-selected only account-0 UTXOs) — the "spendable" promise was false. Two-part fix: - addUtxoAccount now keys the cached row by the account-level BIP32 path instead of the xpub, so the account INDEX survives. The spend path needs it to rebuild per-input signing paths for account > 0 (utxo.ts rewrites the first 3 addressNList segments from the UTXO's source account path). Dedup is still one row per (scriptType, account) via the (device,chain,path) upsert; the xpub stays in its own column for the display merges. - buildTx's non-BTC UTXO branch now merges the device-cached account-1+ xpubs into derivedXpubs/allXpubs, each carrying its parsed account path, so buildUtxoTx aggregates their UTXOs and signs them with the correct key. Device-scoped, gated !isPassphraseWallet, dedup by xpub. Device smoke still required: track an account-1-funded LTC/DOGE/DASH, then send consuming those UTXOs and confirm the signature recovers to the device. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(audit): harden UTXO spend to aggregate for any account>0 xpub Defense-in-depth for the account-1 spend path: use multi-xpub aggregation whenever any derived xpub is account > 0, not only when there are 2+ xpubs. The single-xpub path ignores per-xpub accountPath, so a lone account-N xpub (account-0 derivation returned nothing but a tracked account-1 row exists) would sign with blockbook's account-0 path — a wrong key. Aggregation tags each input with its source account path, guaranteeing account-N signs with the account-N key. Not reachable in practice (a connected device derives account 0 reliably), but this closes the one design dependency. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(swap): aggregate tracked account-N UTXOs in NEAR Intents sendMax estimator headlessSwapQuote's sendMax fee estimator derived only the default account-0 xpub for non-BTC UTXO chains, so a funded account 1 with an empty account 0 produced no UTXOs → estimateUtxoFee returned null → the net-amount re-quote was skipped → the NEAR Intents sendMax deposit could under-deliver, even though getBalances now surfaces (and buildTx now spends) those account-N funds. Merge the device-cached account-1+ xpubs into estXpubs here too, mirroring the buildTx merge: each carries its parsed account path, gated !isPassphraseWallet, dedup by xpub. Aggregation kicks in only when a cached account-N xpub exists, so the account-0-only path is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#308) hdwallet does not strip the EIP-1559 fee fields, so defaulting an absent maxPriorityFeePerGas to '0x0' reaches the device as a literal [0x00] byte. The firmware hashes that non-canonically (a zero RLP integer must be the empty string 0x80), so the signature is over a pre-image no standard encoder reproduces and the tx recovers to the wrong signer / is unbroadcastable. Root-caused on-device (fw 7.15.0): a zero-priority EIP-1559 tx recovered to a random address (keepkey-sdk tests/evm-firmware/eip1559-recover.js). Immediate unblock; the authoritative firmware fix (canonicalise zero RLP fields) is in keepkey-firmware PRs to alpha + develop. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…etach kickout) (#310) getBalances derives every non-EVM chain's address on each refresh. Hive was added ahead of the firmware that implements HiveGetPublicKey (msg 1600), so on firmware without it the device replies FAILURE "Unknown message" and that unrecognized round-trip drops the device off the USB bus — re-enumerating it roughly every 2 minutes while idle and bouncing the user from the dashboard back to the wallet picker. - Probe once per device+firmware, cache the FAILURE_UnexpectedMessage miss, and skip that chain until the firmware version changes. Only the specific unknown-message failure is cached, so transient/device-busy errors still retry. - Capture the capability key + device id BEFORE the derivation call: a failure can clear cachedFeatures, and recomputing in the catch would store a mismatched miss (unknown:?:chain), letting the next refresh re-send. - Apply the same gate to the single-chain getBalance path (e.g. drilling into Hive to Receive) so it can't re-trigger the detach. - Drop any stale cached balance for a chain when first marked unsupported, so a row cached under earlier firmware can't linger in the dashboard. Verified on device (fw 7.15.0 candidate): Hive probed exactly once, skipped thereafter, 0 USB detaches across the idle window (was ~1 every 2 min). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rd): always-show default chains, EVM-only Add chain (#311) * fix(onboarding): reopen setup wizard when a completed device returns to needs_init wizardComplete is only ever cleared by Dashboard (via onForceRefreshConsumed), and Dashboard only mounts in the "ready" phase. If the wizard set wizardComplete=true and the device then went to needs_init instead of ready (wiped, or rebooted uninitialized after a firmware flash), Dashboard never mounts, wizardComplete stays true, oobEnteredRef is forced false, and a needs_init device falls through every phase branch to the default "splash" — the wallet-picker — so the OOB wizard can never reopen. Clear wizardComplete whenever the device is in a genuine setup state (bootloader / needs_firmware / needs_init) so the wizard remounts. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(dashboard): always show default chains; scope "Add chain" to custom EVMs The sidebar hid empty-balance default chains behind an "Add a blockchain" grid (ChainPickerDialog), while the custom-EVM AddChainDialog — the only thing that actually needs adding — was unreachable: its sole trigger lived inside a dead `{false && <SimpleGrid>}` card view. - sidebarChains = sortedChains: every default chain is always shown (funded first), no per-chain balance gating. Removes hiddenChains + the session-only manuallyShown/showChainPicker state. - The sidebar "+" button now opens AddChainDialog (custom EVM: browse Pioneer catalog or manual chainId+RPC), relabeled "Add EVM chain". - Delete the orphaned ChainPickerDialog and the pre-existing dead `{false && <SimpleGrid>}` grid-card block (and its unused SimpleGrid import). Net -224 lines in Dashboard.tsx. Custom-EVM backend (browseChains / addCustomChain / getCustomChains, custom_chains table) is unchanged — this only revives its UI entry point and drops the built-in chain-picker gating. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…312) * fix(hive): surface real backend error on account-create failure The account-create catch hardcoded 'device or network error', masking the actual cause (hdwallet has no hiveSignAccountCreate binding, so the wallet call throws 'is not a function'). This misattribution sent debugging toward Pioneer/network. Surface e.message so the real reason is visible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(hive): gate ETH anti-drain gate off until Pioneer implements it Vault signed an EIP-191 ETH gate and sent ethAddress/ethSignature, but deployed Pioneer never built the server side — TSOA rejects the excess fields (confirmed via prod logs), so account creation always 400'd and the user got a pointless ETH confirm on-device first. Add in-code flag HIVE_ETH_GATE=false: skip the ETH signing + omit the fields so the request matches deployed Pioneer. Also log Pioneer's exact reject reason on any non-200. Flip back on once Pioneer ships the gate (docs/handoff-pioneer-hive-eth-gate.md). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Flip HIVE_ETH_GATE=true — the vault signs the EIP-191 gate and sends ethAddress/ethSignature again. Releasing with the gate on. HARD DEPENDENCY: requires Pioneer's server-side ETH gate deployed (accept + verify the fields) AND HIVE_ONBOARDING_ENABLED=true, or create-account 400s on the excess fields. See docs/handoff-pioneer-hive-eth-gate.md. Re-enables what #312 gated off. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ent failures, stale shadows) (#314) Root cause of "sent HIVE, pressed refresh on asset page, nothing happened": the single-chain getBalance derive drops result.publicKey, so hive (hiveGetPublicKey returns { publicKey }, no address) threw "Could not derive address for Hive" before ever reaching Pioneer — and AssetPage swallowed the rejection into console.warn. The bulk getBalances path has the fallback, which is why dashboard refresh-all worked. Audit verified the forceRefresh flag itself is sound end-to-end (client wire → ?forceRefresh=true → Pioneer cache bypass, hive included). - bun getBalance: add `|| result?.publicKey` fallback (mirrors getBalances) - bun getBalance: write back with force=true — a Pioneer-confirmed result (incl. genuine zeros / unpriced balances) must overwrite stale cache, same rule as getBalances' confirmedChainIds - AssetPage: delete refreshedBalance — it permanently shadowed newer balance-prop data (pushes, bulk refreshes, post-send resyncs) for the rest of the mount; the backend already pushes 'balance-updated' which flows back via the prop - AssetPage: surface refresh failures (rose chip + error tooltip) instead of silently stopping the spinner under a green "Synced" chip; 90s rpc timeout to cover the backend's 60s Pioneer budget (was 30s default) - AssetPage: delete the scoped tx-push subscription — duplicate of the always-mounted App.tsx listener (double forced fetch per push, and its chain-matching was bypassed by 'confirmed' pushes); drop the redundant BTC refreshBtcAccounts round-trip ('btc-accounts-update' push covers it) - AssetPage: hide the refresh button in watch-only (device required), format the mobile balance row - HiveAccountPanel: refresh button now re-fetches HIVE/HBD/HP via a refreshNonce prop (was fetch-once-per-key, unreachable by any refresh) - Dashboard: key AssetPage by chain id so chain switches remount cleanly - Soft-path hygiene: EVM autoDiscover runs once per session with a soft Pioneer read (was: device re-derives + forced scans on every getBalances, incl. page loads); address-book seeding no longer forces - i18n: refreshFailed key, all 14 locales - docs: pioneer-side handoff (hive push coverage, degraded fetchedAt rewrite, hive adapter hardening) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n paths; repair the push pipeline (#316) getBalance was a ~470-line hand-mirrored fork of getBalances and every one of the six drift bugs the refresh audit found lived in a mirrored leaf. Extract the leaves into shared module helpers so the paths cannot diverge again (locality kept — orchestration stays per-handler): - deriveChainAddress(): param shape, ripple quirk, { address } vs { publicKey } result shapes (hive), unknown-message capability recording + stale-cache-row drop — used by both handlers (getBalance previously never recorded misses, so it kept re-sending rejected messages — the #310 USB-detach class) - isTokenEntry / parseTokenEntry / CONTRACT_CAIP_RE: token classification + parsing (getBalance and refreshWatchOnlyBalances regain the denom|bank contract match — TCY/RUJI — and the caip-prefix networkId fallback) - dedupeTokensByCaip: the byte-identical dedup loop existed 5 times - mapServerDefiPosition: the DeFi mapping existed 3 times - fetchShieldedZecToken: zcash shielded zZEC merge — getBalance now attaches it too (a single-chain zcash refresh used to wipe the cached shielded sub-row via its tokens_json overwrite) getBalance also now: - honors response meta: degraded Pioneer data surfaces on the warning banner and is treated as UNCONFIRMED (no force cache overwrite — a degraded zero must not clobber a good cached balance) - applies the same firmware/feature-flag chain gates as getBalances - takes an optional forceRefresh param (default TRUE — user refresh / post-send / tx pushes must bypass Pioneer's cache) Push pipeline repair (socket leg): - Parse JSON-stringified payloads and resolve the chain from caip/networkId (the server's 'chain' field is symbol-ish, so genuine watchtower 'transaction:incoming' events were mostly dropped); map the server's payload types into the schema union ('confirmation_update' → 'confirmed') so the incoming toast works from this leg. - Deliberately consume ONLY 'transaction:incoming'. Pioneer's 'balance:update'/'balance:cache:update' fire from INSIDE its GetPortfolioBalances controller per request, to the requester's own socket — consuming them creates a self-sustaining refresh feedback loop (caught in adversarial review; documented in docs/handoff-pioneer-hive-push-refresh.md §7 for the server-side fix). - Dedup pushes by txid across the SSE and socket legs (one deposit no longer double-toasts and double-fetches); merge pending debounced payloads so a confirmation update can't eat an incoming-payment toast. - Debounce map hoisted to module scope and cleared on disconnect (pending timers used to fire getBalance at an unplugged device); SSE tx:confirmed forwards now debounced per network (they re-fire per confirmation, each costing a forced fetch). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
broadcastTx requires signedTx.hiveBuildResult to assemble the Hive
broadcast body (ref block, expiration, from/to, amount), but the
hiveSignTx RPC handler only returned {signature, serializedTx},
dropping it. Every Hive send failed at broadcast with "Hive broadcast
requires hiveBuildResult in signedTx". Re-attach it from params,
matching the existing tronSignTx/tonSignTx handlers which already
pass their own build-result fields through the same way.
Resyncing the balance immediately after broadcast re-reads the balance server before it has indexed the just-broadcast tx, so the resync briefly overwrites the UI with the pre-send balance. Delay the resync so it lands after the indexer has caught up, and snapshot the chainId at broadcast time so the resync stays gated to the network the tx was actually sent from even if the form has since moved on.
…art (#319) `cmd /c start` re-parses its command line with cmd.exe's own quoting rules, which don't understand backslash-escaped quotes the way Bun.spawn's Win32 argv-escaping does. The two layers collide and mangle the URL into a stray leading backslash ("Windows cannot find '\https://keepkey.com/update?os=windows'"), breaking the update download link and general external-link opening on Windows. Switch to `rundll32 url.dll,FileProtocolHandler <url>`, which takes the URL as a normal argv element with no shell re-parsing, so both the `&` in query strings and Bun's escaping work correctly. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…not the ceiling (#320) A USDT send failed on-chain as "Failed — Out of Energy" (c105241aee4b1c77cee43e24717741dffc74ed91b35475ce4b5153497b463baf), burning the full 30 TRX fee_limit with no transfer. The 30 TRX cap is below the real cost of a transfer to a zero-USDT recipient once TRON's dynamic-energy penalty on the USDT contract kicks in (~130k energy base + penalty up to 3.4x, >30 TRX at peak). - fee_limit → 100 TRX (ceiling only; actual consumption is charged; covers the max-penalty worst case; matches TronLink's USDT default) - displayed fee is now a triggerconstantcontract simulation ((energy_used + energy_penalty) × getEnergyFee) instead of the ceiling, falling back to the ceiling if estimation fails Verified against live TronGrid with the failed tx's exact parameters: energy_used=130285, energy_penalty=100635, getEnergyFee=100 sun → ~23 TRX real cost today, confirming 30 was too low and 100 is safe. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
XrpSignRequest used .strip() but never declared tx or lastLedgerSequence in its shape, so both were deleted from the request body before reaching hdwallet.rippleSignTx, which reads msg.tx.value.fee/msg and msg.lastLedgerSequence. Every REST/SDK-driven XRP sign threw "undefined is not an object (evaluating 'msg.tx.value')". Swagger and hdwallet both require the fields; the schema was the outlier. The in-app send path signs over IPC and bypasses this schema, so it shipped uncaught. Verified end-to-end on a real device: address derive -> build -> on-device confirm -> valid signature + correct txid/fields. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…address (#321) Litecoin "stranded funds" end-to-end fix + recovery tooling: - utxoAccountScriptPaths (chain-scan): per-account xpub set now includes the chain's own receive convention AND the LTC legacy p2wpkh-on-BIP44 branch, so pre-1.4.10 addresses stay visible and spendable everywhere (getBalances, buildTx, audit account scans, addUtxoAccount). LTC default path -> BIP84. - AssetPage: LTC script-type picker (44/49/84) for receive addresses; UTXO altcoin multi-account selector (UtxoAccountSelector) backed by the pubkey cache; Synced-at badge only stamps data it can vouch for. - Audit "Scan uncommon paths" grid for LTC (p2wpkh-on-44 / 44-p2pkh / 84- p2wpkh) via auditScanPaths + scriptType. - NEW auditSweepPath RPC + AuditSweepButton: funded uncommon-path finds get a "Sweep to main wallet" button right in the audit flow — signs with the exact found path (no xpub/tracking machinery, nothing persisted), sends max-minus-fee to the chain's standard receive address. Dry-run quote, then sign + broadcast. Re-derives the address on the CURRENTLY connected device before building (device-swap guard) and re-checks the wallet handle before signing. Only uncommon scheme rows offer it — the standard receive branch is the destination, never a source (backend also refuses dest==source). sweep-engine generalized to take coin/networkId (BTC callers unchanged); fee-rate fetch falls back GetFeeRateByNetwork -> GetFeeRate like the send path (was silently defaulting to 5 sat/vB). - Patient audit balance checks: 5 attempts, 30s apart, before any row shows "couldn't verify" — degraded-empty (200-but-failed upstream) retries the same way instead of failing instantly. UI scan timeouts raised to 15 min. Device-verified on-chain (LTC): swept 0.00635104 LTC from uncommon m/44'/2'/0'/0/0 (p2wpkh) to the standard BIP84 receive address, txid 5d4c29093bf6f8ff61aae603c7e2a906cdbf9d3e93190c43cc400fa1fece83fb. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…cing to 0 (#322) * fix(portfolio): sum multi-xpub UTXO chains per pubkey in bulk getBalances The bulk dashboard refresh matched non-BTC UTXO natives by caip, but LTC sends 4+ xpubs (p2pkh/p2sh-p2wpkh/p2wpkh + legacy 44'-p2wpkh + tracked accounts) that all share one caip, and Pioneer returns one row PER pubkey. The caip find() grabbed the chain's first row — typically the empty legacy Ltub — so the dashboard wrote LTC=0 while the single-chain getBalance path (which sums by pubkey) wrote the real total, making the displayed balance bounce between 0 and correct on alternate refreshes. Mirror the BTC aggregation: match each entry strictly by its own pubkey, sum across the chain's xpubs, and push one row per chain. If Pioneer returned no row for any of a multi-xpub chain's pubkeys, drop the chain from confirmedChainIds so an unvalidated 0 never overwrites the cache. Verified against live Pioneer: 4 LTC xpubs → per-pubkey rows summing to 0.02225830, matching the single-chain path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portfolio): confirm multi-xpub chain only when every pubkey chunk succeeded Review finding: if one LTC xpub chunk succeeds while a tracked-account xpub chunk fails, agg.matched stayed true and the partial (lower) sum was force-written as confirmed. Track per-chain allOk from failedPubkeySetForDb — mirroring the BTC every-pubkey rule — so partial aggregates go through the guarded upsert and never overwrite a good cached balance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…signed (#323) After flashing custom firmware in the setup wizard, the device reboots into the new firmware (e.g. test 7.15.0, needs_init) but the wizard stays stranded on the Firmware step, offering a downgrade to the release channel (7.15.0 -> 7.14.1) and polling for bootloader entry forever. Root cause: the custom-flash completion handler set rebootPhase='firmware-rebooting' but not firmwareJustFlashed (the official-update path sets both). The reboot-advance effect then fired on stale cached bootloader features, cleared rebootPhase before its s==='bootloader' early-return, and nothing was left to route the wizard once the device came back. Fixes: - set firmwareJustFlashed on custom-flash completion, so the existing post-flash advance effect routes to init/complete like the official path - auto-skip the Firmware step when needsFirmwareUpdate is false and the device is not in bootloader (mirrors the bootloader auto-skip) — never offer a downgrade when running firmware is current or newer - show a SIGNED/UNSIGNED badge next to CURRENT version on the Firmware step (deviceStatus.firmwareVerified: running version known to the release manifest) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ight (#324) * fix(ui): restore LOW GAS warning for all chain families + swap pre-flight The dashboard LOW GAS badge (added 2a767ba, Apr 2026) went dark in the May UI revamp (39d1ca5 wrapped the old card grid in {false && ...}) and the dead block was deleted in #311 — the sidebar rows that replaced the cards never got the badge. It was also EVM-only. Restore it on the sidebar chain rows for EVERY family: tokens stranded on a chain (>$1) with dust native (<$1) get a red gas-pump + LOW GAS tag (TRC-20 needs TRX, ERC-20 needs ETH, SPL needs SOL, ...). SwapDialog gets the messaging it never had: swapping a TOKEN from a dust-native account signs on-device and then dies at broadcast (TRON: "Account resource insufficient error"). Warn in the FROM panel before the user signs. Warning, not a gate — staked resources (TRON energy) can make fees free with zero native balance. SendForm's existing token-send banner already covers all families and is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(swap): judge low-gas on the selected EVM signing address, not the aggregate Review: the chain-aggregate nativeBalanceUsd can hide a dust-native signing address behind a funded sibling — the selected address is what actually pays gas at broadcast. Mirror fromBalance's selected-address lookup, falling back to the aggregate for non-EVM/untracked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The QR overlay (WalletConnect pairing) had camera + image-file. This adds "scan my screen": Bun minimizes the window, screenshots every display with the OS-native tool, and the webview decodes the PNGs with the existing jsQR path. Native because WKWebView auto-denies getDisplayMedia once the app implements the media-capture delegate Electrobun uses for the camera. macOS permissions (the pain point) — see docs/scan-screen-permissions.md: - Screen Recording is pure TCC; no codesign entitlement exists. - CGPreflight/CGRequestScreenCaptureAccess via bun:ffi; denied requests deep-link the exact System Settings pane. Relaunch needed after granting. - NSScreenCaptureUsageDescription injected by patch-electrobun.sh (covers dev + prod bundles, same mechanism as the camera string). - Dev builds: TCC keys the grant to the responsible process, i.e. your terminal/IDE — grant Screen Recording there once; prod grants stick to the Developer ID-signed bundle. - Unauthorized screencapture fails loudly (verified on macOS 26), so a permission gap can't masquerade as "no QR found". Windows: PowerShell CopyFromScreen per monitor, no permission gate. Linux: grim / gnome-screenshot / spectacle / import, first available. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… 7.15+) (#326) * fix(clearsign): send full unsigned tx to /descriptors/sign, decode base64 blob, gate on VERIFIED The committed pioneer clear-sign path fetched the signed blob with only {chainId, contractAddress, data} and attached it as a hex string. Two bugs surfaced on-device (rc3, R715rc3): 1. hdwallet's ethSignTx arrayify()s a STRING signedPayload as hex, but pioneer emits base64 → "invalid hexadecimal string" thrown before the device renders anything (looked like "nothing happens after approve"). Fix: decode base64 → Uint8Array so hdwallet takes the bytes branch. 2. The blob's tx_hash binds the full sighash (nonce/gas/value/fees), so a 3-field request can never produce a blob rc3 accepts. Fix: thread the real unsigned-tx fields (mirroring the ethSignTx msg construction exactly) into the /descriptors/sign request. Also gate attach on classification === 'VERIFIED' — an OPAQUE/UNKNOWN blob doesn't enable clear-sign (rc3 fail-closes) and attaching it would mask the honest raw-hex/AdvancedMode path. Adds insightClassification to the type. Device-verified: base64 fix lets the metadata reach the device and return an ack; remaining MALFORMED verdict is a pioneer-insight serializer / firmware format issue, tracked separately (not a vault bug). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(clearsign): gate EVM insight behind EVM_INSIGHT flag (default off, fw 7.15+) The Pioneer signed-blob clear-sign path (blob fetch + attach to EthereumSignTx) was unconditionally active. Gate it behind a new build-time flag EVM_INSIGHT (default false) AND the connected device's firmware >= 7.15.0, checked at both chokepoints: - blob FETCH: the txFields that trigger fetchPioneerSignedBlob (rest-api.ts) - blob ATTACH: msg.txMetadata, incl. the caller-provided branch — this reads the blob straight from the request body, so it must be gated independently or a caller could inject a blob and force clear-sign while disabled. Local calldata decode + the blind-sign warning stay live on all firmware. New shared/flags.ts is importable by both bun/ backend and mainview/ frontend. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(clearsign): summarize Uint8Array signedPayload in the payload log JSON.stringify(msg, null, 2) serializes a Uint8Array as a per-byte index map — with a Pioneer blob attached that's hundreds of junk lines per /eth/sign-transaction. Replacer collapses it to Uint8Array(<len>). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
v1.4.10 — 37 commits since v1.4.9
Portfolio & balances
Audit / recovery
Signing
tx+lastLedgerSequence(fix(xrp): REST /xrp/sign-transaction schema strips tx + lastLedgerSequence #309)0x0) for zero/absent priority fee (fix(eth): send empty (not 0x0) for a zero/absent EIP-1559 priority fee #308)EVM_INSIGHT = false+ fw ≥ 7.15 gate (feat(clearsign): EVM insight behind EVM_INSIGHT flag (default off, fw 7.15+) #326)Hive
UX / onboarding
Windows
cmd start(fix(windows): stop corrupting the update/external-link URL via cmd start #319)Zcash
Publish checklist
release/1.4.10(all platforms + signed macOS)🤖 Generated with Claude Code